3.2 KernelLoader

KernelLoader 是连接 Dart 源代码(经过前端编译器处理后的内核格式)和 DartVM 运行时的桥梁。它负责将静态的程序表示转换为可以被 VM 执行的动态结构。这个类的设计和实现直接影响了 DartVM 的性能、内存使用和功能支持(如热重载、调试等)。

KernelLoader 是 DartVM 中负责加载和处理 Dart 内核(Kernel)格式代码的关键类。它的主要作用包括:

  1. 加载程序:通过 LoadProgram 方法加载整个 Dart 程序。这个方法会解析内核格式的代码,创建相应的 VM 内部结构(如类、函数等)。

  2. 加载库LoadLibrary 方法用于加载单个库。这对于增量编译和热重载等场景非常重要。

  3. 加载表达式LoadExpressionEvaluationFunction 方法用于加载和编译表达式评估函数,这在调试器中进行表达式求值时很有用。

  4. 查找修改的库FindModifiedLibraries 方法用于在增量编译时识别哪些库被修改了,这对于有效地更新代码很重要。

  5. 处理类和函数: 包含了处理类(LoadClass)和函数(LoadProcedure)的方法,这些方法负责将内核格式的类和函数定义转换为 VM 内部的表示。

  6. 管理元数据:处理注释(ReadVMAnnotations)和其他元数据,这些信息对于反射和工具支持很重要。

  7. 处理常量:包含了读取和处理编译时常量的逻辑,这对于优化和静态分析很重要。

  8. 类型处理:包含了处理类型参数、协变性等与 Dart 类型系统相关的逻辑。

  9. 内存管理:管理加载过程中的内存分配,确保高效和正确的内存使用。

  10. 错误处理:提供了错误报告和处理机制,确保在加载过程中遇到问题时能够优雅地处理。


LoadEntireProgram

kernel::KernelLoader::LoadEntireProgram将 Kernel AST 反序列化为相应 VM 对象的入口点。方法签名,接受一个 Program 指针和一个布尔值参数:

Object& KernelLoader::LoadEntireProgram(Program* program,
                                        bool process_pending_classes) {

前置准备:

// 获取当前线程的指针。
Thread* thread = Thread::Current();
// 这是一个性能计时宏,用于记录加载内核的时间。
TIMELINE_DURATION(thread, Isolate, "LoadKernel");

如果是单个程序,则创建一个 KernelLoader 实例并直接加载程序。

  if (program->is_single_program()) {
    KernelLoader loader(program, /*uri_to_source_table=*/nullptr);
    return Object::Handle(loader.LoadProgram(process_pending_classes));
  }

如果不是单个程序,创建一个数组来存储子程序的起始位置,然后使用 index_programs 函数来填充这个数组。

  GrowableArray<intptr_t> subprogram_file_starts;
  {
    kernel::Reader reader(program->binary());
    index_programs(&reader, &subprogram_file_starts);
  }

获取当前线程的内存区域,创建一个 Library 句柄,计算子程序的数量。

  Zone* zone = thread->zone();
  Library& library = Library::Handle(zone);
  intptr_t subprogram_count = subprogram_file_starts.length() - 1;

接下来的代码主要是处理多个子程序的情况,包括:

  1. 索引所有源代码表
  2. 为每个子程序创建"fake programs"并加载它们
  3. 如果需要,处理待处理的类

这段代码的主要目的是为所有子程序构建一个统一的源代码表,同时确保不同子程序之间没有冲突的源代码定义。

如果找到了匹配的条目,检查是否存在冲突;如果没有找到,则创建新的条目并插入到映射表中。

// First index all source tables.
// 创建一个 UriToSourceTable 对象,用于存储 URI 到源代码的映射。
UriToSourceTable uri_to_source_table;
// 创建一个 UriToSourceTableEntry 对象,用作临时包装器。
UriToSourceTableEntry wrapper;
// 获取当前线程和其关联的内存区域(Zone)。
Thread* thread_ = Thread::Current();
Zone* zone_ = thread_->zone();
// 开始一个循环,从最后一个子程序开始向前遍历所有子程序。
for (intptr_t i = subprogram_count - 1; i >= 0; --i) {
  // 获取当前子程序的起始和结束位置。
  intptr_t subprogram_start = subprogram_file_starts.At(i);
  intptr_t subprogram_end = subprogram_file_starts.At(i + 1);
  // 创建一个 TypedDataBase 句柄,表示当前子程序的二进制数据。
  const auto& component = TypedDataBase::Handle(
      program->binary().ViewFromTo(subprogram_start, subprogram_end));
  // 创建 TranslationHelper 和 KernelReaderHelper 对象,用于辅助读取内核数据。
  TranslationHelper translation_helper(thread);
  KernelReaderHelper helper_(zone_, &translation_helper, component, 0);
  // 获取源代码表的大小。
  const intptr_t source_table_size = helper_.SourceTableSize();
  // 开始遍历源代码表中的每一项。
  for (intptr_t index = 0; index < source_table_size; ++index) {
    // 获取当前项的 URI 字符串,并将其设置为包装器的 URI。
    const String& uri_string = helper_.SourceTableUriFor(index);
    wrapper.uri = &uri_string;
    // 获取行起始位置数据,如果为空则跳过当前项。
    TypedData& line_starts =
        TypedData::Handle(Z, helper_.GetLineStartsFor(index));
    if (line_starts.Length() == 0) continue;
    // 获取脚本源代码,并再次设置包装器的 URI(这里可能是冗余的)。
    const String& script_source = helper_.GetSourceFor(index);
    wrapper.uri = &uri_string;
    // 在 URI 到源代码的映射表中查找当前 URI。
    UriToSourceTableEntry* pair = uri_to_source_table.LookupValue(&wrapper);
    if (pair != nullptr) {
      // // 检查是否存在冲突的源代码条目
      // At least two entries with content. Unless the content is the same
      // that's not valid.
      const bool src_differ = pair->sources->CompareTo(script_source) != 0;
      const bool line_starts_differ =
          !pair->line_starts->CanonicalizeEquals(line_starts);
      if (src_differ || line_starts_differ) {
        FATAL(/* 错误信息 */);
      }
    } else {
      // // 创建新的条目并插入到映射表中
      UriToSourceTableEntry* tmp = new UriToSourceTableEntry();
      tmp->uri = &uri_string;
      tmp->sources = &script_source;
      tmp->line_starts = &line_starts;
      uri_to_source_table.Insert(tmp);
    }
  }
}

这段代码的主要目的是为每个子程序创建一个"假程序",加载它,并处理加载结果。如果加载成功并且结果是一个库,它会更新全局的 library 变量。这个过程允许 DartVM 处理可能由多个部分组成的复杂程序。

// Create "fake programs" for each sub-program.
// 这个循环为每个子程序创建"假程序"。它从最后一个子程序开始,向前遍历。
for (intptr_t i = subprogram_count - 1; i >= 0; --i) {
  // 获取当前子程序的起始和结束位置。
  intptr_t subprogram_start = subprogram_file_starts.At(i);
  intptr_t subprogram_end = subprogram_file_starts.At(i + 1);
  // 创建一个 TypedDataBase 句柄,表示当前子程序的二进制数据。
  const auto& component = TypedDataBase::Handle(
      program->binary().ViewFromTo(subprogram_start, subprogram_end));
  // 创建一个 Reader 对象来读取这个组件。
  Reader reader(component);
  const char* error = nullptr;
  // 尝试从reader中读取一个Program对象。使用unique_ptr来管理内存。
  std::unique_ptr<Program> subprogram = Program::ReadFrom(&reader, &error);
  // 如果读取失败,输出错误信息并终止程序。
  if (subprogram == nullptr) {
    FATAL("Failed to load kernel file: %s", error);
  }
  // 断言确保读取的是单个程序。
  ASSERT(subprogram->is_single_program());
  // 创建一个KernelLoader对象来加载这个子程序。
  KernelLoader loader(subprogram.get(), &uri_to_source_table);
  // 加载程序并获取结果。
  Object& load_result = Object::Handle(loader.LoadProgram(false));
  // 如果加载结果是错误,直接返回这个错误。
  if (load_result.IsError()) return load_result;
  // 如果加载结果是一个库,更新 library 变量。
  // 这里使用了位异或操作符^=,这在Dart VM中通常用于更新对象引用。
  if (load_result.IsLibrary()) {
    library ^= load_result.ptr();
  }
}

最后,根据处理结果返回适当的对象(库或错误)。

if (process_pending_classes && !ClassFinalizer::ProcessPendingClasses()) {
	// Class finalization failed -> sticky error would be set.
	return Error::Handle(thread->StealStickyError());
}

return library;

LoadProgram

这个方法负责加载整个 Dart 程序,包括所有的库、类和常量。它处理了错误情况,确保了线程安全,并设置了程序的初始状态。这是 Dart VM 启动和运行 Dart 程序的关键部分。

这是方法的定义。它接受一个布尔参数 process_pending_classes,返回一个 ObjectPtr。这个方法的目的是加载整个 Dart 程序。

ObjectPtr KernelLoader::LoadProgram(bool process_pending_classes) {

前置检查:

// 这行创建了一个安全点写锁。
// 它确保在加载程序时,其他线程不会修改程序的状态。这是为了线程安全。
SafepointWriteRwLocker ml(thread_, thread_->isolate_group()->program_lock());

// 这个断言检查 `kernel_program_info_` 的常量数组是否为空。
// 这确保我们还没有加载任何常量。
ASSERT(kernel_program_info_.constants() == Array::null());

// 这个检查确保我们不是在尝试加载一个连接的 dill 文件
// (多个 Dart 程序合并成一个文件)。如果是,程序会终止并显示错误消息。
if (!program_->is_single_program())
{
  FATAL(
      "Trying to load a concatenated dill file at a time where that is "
      "not allowed");
}

核心逻辑:

// 这设置了一个长跳转(longjump)作用域。
// 这是用于错误处理的 - 如果在加载过程中发生严重错误,我们可以跳转到这里。
LongJumpScope jump;
if (setjmp(*jump.Set()) == 0)
{
  // Note that `problemsAsJson` on Component is implicitly skipped.
  // 这个循环遍历程序中的所有库,并调用 `LoadLibrary` 方法来加载每个库。
  const intptr_t length = program_->library_count();
  for (intptr_t i = 0; i < length; i++)
  {
    LoadLibrary(i);
  }

  // 如果 `process_pending_classes` 为真,这段代码会处理所有待处理的类。
  // 如果处理失败,它会返回一个错误。
  // Finalize still pending classes if requested.
  if (process_pending_classes)
  {
    if ProcessPendingClasses()
    {
      // Class finalization failed -> sticky error would be set.
      return H.thread()->StealStickyError();
    }
  }

  // Sets the constants array to an empty array with the length equal to
  // the number of constants. The array gets filled lazily while reading
  // constants.
  // 这段代码为常量创建一个数组。
  // 它首先检查常量表是否存在,然后创建一个新的数组来存储所有常量。
  // 初始时,所有元素都被设置为哨兵对象。
  ASSERT(kernel_program_info_.constants_table() != ExternalTypedData::null());
  ConstantReader constant_reader(&helper_, &active_class_);
  // 所有常量的数量
  const intptr_t num_consts = constant_reader.NumConstants();
  // 存储常量的数组
  const Array &array = Array::Handle(Z, Array::New(num_consts, Heap::kOld));
  for (intptr_t i = 0; i < num_consts; i++)
  {
    array.SetAt(i, Object::sentinel());
  }
  kernel_program_info_.set_constants(array);
  H.SetConstants(array); // for caching

  // 这段代码查找程序的主方法。
  // 如果找到了主方法,它会返回包含主方法的库。如果没有主方法,它返回 null。
  NameIndex main = program_->main_method();
  if (main != -1)
  {
    NameIndex main_library = H.EnclosingName(main);
    return LookupLibrary(main_library);
  }

  return Library::null();
}

// Either class finalization failed or we caught a compile error.
// In both cases sticky error would be set.
// 如果发生了长跳转(即出现了错误),这行代码会返回当前线程的"粘性"错误。
return Thread::Current()->StealStickyError();

本文作者:Maeiee

本文链接:3.2 KernelLoader

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!